Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Plugins / Purple Service / adiumPurpleRequest.m
blobb2a459db689f4af69fa1322da8e3dc4c31ca1d27
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
18 #import "adiumPurpleRequest.h"
19 #import "ESPurpleRequestActionController.h"
20 #import "ESPurpleRequestWindowController.h"
21 #import "ESPurpleFileReceiveRequestController.h"
22 #import <Adium/NDRunLoopMessenger.h>
23 #import <Adium/AIContactAlertsControllerProtocol.h>
24 #import <AIUtilities/AIObjectAdditions.h>
25 #import <Adium/ESFileTransfer.h>
26 #import "AMPurpleRequestFieldsController.h"
28 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
29 #import "AILibpurplePlugin.h"
30 #import <libintl/libintl.h>
33  * Purple requires us to return a handle from each of the request functions.  This handle is passed back to use in 
34  * adiumPurpleRequestClose() if the request window is no longer valid -- for example, a chat invitation window is open,
35  * and then the account disconnects.  All window controllers created from adiumPurpleRequest.m should return non-autoreleased
36  * instances of themselves.  They then release themselves when their window closes.  Rather than calling
37  * [[self window] close], they should use purple_request_close_with_handle(self) to ensure proper bookkeeping purpleside.
38  */
40 //Jabber registration
41 #include <libpurple/jabber.h>
43 /* resolved id for Meanwhile */
44 struct resolved_id {
45         char *id;
46         char *name;
49 /*!
50  * @brief Process button text, removing gtk+ accelerator underscores
51  *
52  * Textual underscores are indicated by "__"
53  */
54 NSString *processButtonText(NSString *inButtonText)
56         NSMutableString *processedText = [inButtonText mutableCopy];
57         
58 #define UNDERSCORE_PLACEHOLDER @"&&&&&"
60         //Replace escaped underscores with our placeholder
61         [processedText replaceOccurrencesOfString:@"__"
62                                                                    withString:UNDERSCORE_PLACEHOLDER
63                                                                           options:NSLiteralSearch
64                                                                                 range:NSMakeRange(0, [processedText length])];
65         //Remove solitary underscores
66         [processedText replaceOccurrencesOfString:@"_"
67                                                                    withString:@""
68                                                                           options:NSLiteralSearch
69                                                                                 range:NSMakeRange(0, [processedText length])];
71         //Replace the placeholder with an underscore
72         [processedText replaceOccurrencesOfString:UNDERSCORE_PLACEHOLDER
73                                                                    withString:@"_"
74                                                                           options:NSLiteralSearch
75                                                                                 range:NSMakeRange(0, [processedText length])];
76         
77         return [processedText autorelease];
78         
81 static void *adiumPurpleRequestInput(
82                                                                    const char *title, const char *primary,
83                                                                    const char *secondary, const char *defaultValue,
84                                                                    gboolean multiline, gboolean masked, gchar *hint,
85                                                                    const char *okText, GCallback okCb, 
86                                                                    const char *cancelText, GCallback cancelCb,
87                                                                    PurpleAccount *account, const char *who, PurpleConversation *conv,
88                                                                    void *userData)
90         /*
91          Multiline should be a paragraph-sized box; otherwise, a single line will suffice.
92          Masked means we want to use an NSSecureTextField sort of thing.
93          We may receive any combination of primary and secondary text (either, both, or neither).
94          */
95         id                                      requestController = nil;
96         NSString                        *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
97         
98         //Ignore purple trying to get an account's password; we'll feed it the password and reconnect if it gets here, somehow.
99         if ([primaryString rangeOfString:@"Enter password for "].location != NSNotFound) return [NSNull null];
100         
101         NSMutableDictionary *infoDict;
102         NSString                        *okButtonText = processButtonText([NSString stringWithUTF8String:okText]);
103         NSString                        *cancelButtonText = processButtonText([NSString stringWithUTF8String:cancelText]);
104         
105         infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:okButtonText,@"OK Text",
106                 cancelButtonText,@"Cancel Text",
107                 [NSValue valueWithPointer:okCb],@"OK Callback",
108                 [NSValue valueWithPointer:cancelCb],@"Cancel Callback",
109                 [NSValue valueWithPointer:userData],@"userData",nil];
110         
111         
112         if (primaryString) [infoDict setObject:primaryString forKey:@"Primary Text"];
113         if (title) [infoDict setObject:[NSString stringWithUTF8String:title] forKey:@"Title"];  
114         if (defaultValue) [infoDict setObject:[NSString stringWithUTF8String:defaultValue] forKey:@"Default Value"];
115         if (secondary) [infoDict setObject:[NSString stringWithUTF8String:secondary] forKey:@"Secondary Text"];
116         
117         [infoDict setObject:[NSNumber numberWithBool:multiline] forKey:@"Multiline"];
118         [infoDict setObject:[NSNumber numberWithBool:masked] forKey:@"Masked"];
119         
120         AILog(@"adiumPurpleRequestInput: %@",infoDict);
121         
122         requestController = [ESPurpleRequestWindowController showInputWindowWithDict:infoDict];
123         
124         return (requestController ? requestController : [NSNull null]);
127 static void *adiumPurpleRequestChoice(const char *title, const char *primary,
128                                                                         const char *secondary, int defaultValue,
129                                                                         const char *okText, GCallback okCb,
130                                                                         const char *cancelText, GCallback cancelCb,
131                                                                         PurpleAccount *account, const char *who, PurpleConversation *conv,
132                                                                         void *userData, va_list choices)
134         AILog(@"adiumPurpleRequestChoice: %s\n%s\n%s ",
135                            (title ? title : ""),
136                            (primary ? primary : ""),
137                            (secondary ? secondary : ""));
138         
139         return [NSNull null];
142 //Purple requests the user take an action such as accept or deny a buddy's attempt to add us to her list 
143 static void *adiumPurpleRequestAction(const char *title, const char *primary,
144                                                                         const char *secondary, int default_action,
145                                                                         PurpleAccount *account, const char *who, PurpleConversation *conv,
146                                                                         void *userData,
147                                                                         size_t actionCount, va_list actions)
149     NSString                    *titleString = (title ? [NSString stringWithUTF8String:title] : @"");
150         NSString                        *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
151         id                                      requestController = nil;
152         int                                     i;
153         BOOL                            handled = NO;
154         
155         if (primaryString && ([primaryString rangeOfString:@"has just asked to directly connect"].location != NSNotFound)) {
156                 AIListContact *adiumContact = contactLookupFromBuddy(purple_find_buddy(account, who));
157                 //Automatically accept Direct IM requests from contacts on our list
158                 if (adiumContact && [adiumContact isIntentionallyNotAStranger]) {
159                         GCallback ok_cb;
161                         //Get the callback for Connect, skipping over the title
162                         va_arg(actions, char *);
163                         ok_cb = va_arg(actions, GCallback);
164                         
165                         ((PurpleRequestActionCb)ok_cb)(userData, default_action);
166                         
167                         handled = YES;
168                 }
169         } else if (primaryString && ([primaryString rangeOfString:@"Accept chat invitation"].location != NSNotFound)) {
170                 AIListContact *contact = contactLookupFromBuddy(purple_find_buddy(account, who));
171                 [[[AIObject sharedAdiumInstance] contactAlertsController] generateEvent:CONTENT_GROUP_CHAT_INVITE
172                                                                                  forListObject:contact
173                                                                                           userInfo:[NSDictionary dictionary]
174                                                   previouslyPerformedActionIDs:nil];    
175         }
177         if (!handled) {
178                 NSString                *secondaryString = (secondary ? [NSString stringWithUTF8String:secondary] : nil);
179                 NSMutableArray  *buttonNamesArray = [NSMutableArray arrayWithCapacity:actionCount];
180                 GCallback               *callBacks = g_new0(GCallback, actionCount);
181                 
182                 //Generate the actions names and callbacks into useable forms
183                 for (i = 0; i < actionCount; i += 1) {
184                         char *buttonName;
185                         
186                         //Get the name
187                         buttonName = va_arg(actions, char *);
188                         [buttonNamesArray addObject:processButtonText([NSString stringWithUTF8String:buttonName])];
189                         
190                         //Get the callback for that name
191                         callBacks[i] = va_arg(actions, GCallback);
192                 }
193                 
194                 //Make default_action (or first if none specified) the last one
195                 if (default_action < (int)actionCount-1) {
196                         // If there's no default_action, assume the first one is, and move it to the end.
197                         if (default_action == -1)
198                                 default_action = 0;
200                         GCallback tempCallBack = callBacks[actionCount-1];
201                         callBacks[actionCount-1] = callBacks[default_action];
202                         callBacks[default_action] = tempCallBack;
203                         
204                         [buttonNamesArray exchangeObjectAtIndex:default_action withObjectAtIndex:(actionCount-1)];
205                 }
206                 
207                 NSMutableDictionary     *infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
208                                                                                  buttonNamesArray,@"Button Names",
209                                                                                  [NSValue valueWithPointer:callBacks],@"callBacks",
210                                                                                  [NSValue valueWithPointer:userData],@"userData",
211                                                                                  titleString,@"TitleString",nil];
212                 
213                 // If we have both a primary and secondary string, use the primary as a header.
214                 if (secondaryString) {
215                         [infoDict setObject:primaryString forKey:@"MessageHeader"];
216                         [infoDict setObject:secondaryString forKey:@"Message"];
217                 } else {
218                         [infoDict setObject:primaryString forKey:@"Message"];
219                 }
220                 
221                 AIAccount *adiumAccount = accountLookup(account);
222                 if (adiumAccount) {
223                         [infoDict setObject:adiumAccount forKey:@"AIAccount"];
224                 }
225                 
226                 if (who) {
227                         AIListContact *adiumContact = contactLookupFromBuddy(purple_find_buddy(account, who));
228                         if (adiumContact) {
229                                 [infoDict setObject:adiumContact forKey:@"AIListContact"];
230                         }
231                         
232                         [infoDict setObject:[NSString stringWithUTF8String:who] forKey:@"who"];
233                 }
235                 requestController = [ESPurpleRequestActionController showActionWindowWithDict:infoDict];
236         }
238         return (requestController ? requestController : [NSNull null]);
241 static void *adiumPurpleRequestFields(const char *title, const char *primary,
242                                                                         const char *secondary, PurpleRequestFields *fields,
243                                                                         const char *okText, GCallback okCb,
244                                                                         const char *cancelText, GCallback cancelCb,
245                                                                         PurpleAccount *account, const char *who, PurpleConversation *conv,
246                                                                         void *userData)
248         id                                      requestController = nil;
249         NSString                        *titleString = (title ?  [[NSString stringWithUTF8String:title] lowercaseString] : nil);
251     if (titleString && 
252                 [titleString rangeOfString:@"new jabber"].location != NSNotFound) {
253                 /* Jabber registration request. Instead of displaying a request dialogue, we fill in the information automatically.
254                  * And by that, I mean that we accept all the default empty values, since the username and password are preset for us. */
255                 ((PurpleRequestFieldsCb)okCb)(userData, fields);
256                 
257         } else {
258                 AILog(@"adiumPurpleRequestFields: %s\n%s\n%s ",
259                                    (title ? title : ""),
260                                    (primary ? primary : ""),
261                                    (secondary ? secondary : ""));
262         
263         id self = (CBPurpleAccount*)account->ui_data; // for AILocalizedString
264                 
265         requestController = [[AMPurpleRequestFieldsController alloc] initWithTitle:title?[NSString stringWithUTF8String:title]:nil
266                                                                        primaryText:primary?[NSString stringWithUTF8String:primary]:nil
267                                                                      secondaryText:secondary?[NSString stringWithUTF8String:secondary]:nil
268                                                                      requestFields:fields
269                                                                             okText:okText?[NSString stringWithUTF8String:okText]:AILocalizedString(@"OK",nil)
270                                                                           callback:okCb
271                                                                         cancelText:cancelText?[NSString stringWithUTF8String:cancelText]:AILocalizedString(@"Cancel",nil)
272                                                                           callback:cancelCb
273                                                                            account:(CBPurpleAccount*)account->ui_data
274                                                                                who:who?[NSString stringWithUTF8String:who]:nil
275                                                                       conversation:conv
276                                                                           userData:userData];
277 #if 0
278                 GList                                   *gl, *fl, *field_list;
279                 PurpleRequestFieldGroup *group;
281                 //Look through each group, processing each field
282                 for (gl = purple_request_fields_get_groups(fields);
283                          gl != NULL;
284                          gl = gl->next) {
285                         
286                         group = gl->data;
287                         field_list = purple_request_field_group_get_fields(group);
288                         
289                         for (fl = field_list; fl != NULL; fl = fl->next) {
290                                 /*
291                                 typedef enum
292                                 {
293                                         PURPLE_REQUEST_FIELD_NONE,
294                                         PURPLE_REQUEST_FIELD_STRING,
295                                         PURPLE_REQUEST_FIELD_INTEGER,
296                                         PURPLE_REQUEST_FIELD_BOOLEAN,
297                                         PURPLE_REQUEST_FIELD_CHOICE,
298                                         PURPLE_REQUEST_FIELD_LIST,
299                                         PURPLE_REQUEST_FIELD_LABEL,
300                                         PURPLE_REQUEST_FIELD_ACCOUNT
301                                 } PurpleRequestFieldType;
302                                 */
304                                 /*
305                                 PurpleRequestField              *field;
306                                 PurpleRequestFieldType  type;
307                                 
308                                 field = (PurpleRequestField *)fl->data;
309                                 type = purple_request_field_get_type(field);
310                                 if (type == PURPLE_REQUEST_FIELD_STRING) {
311                                         if (strcasecmp("username", purple_request_field_get_label(field)) == 0) {
312                                                 purple_request_field_string_set_value(field, purple_account_get_username(account));
313                                         } else if (strcasecmp("password", purple_request_field_get_label(field)) == 0) {
314                                                 purple_request_field_string_set_value(field, purple_account_get_password(account));
315                                         }
316                                 }
317                                  */
318                         }
319                         
320                 }
321 //              ((PurpleRequestFieldsCb)okCb)(userData, fields);
322 #endif
323         }
324     
325         return (requestController ? requestController : [NSNull null]);
328 static void *adiumPurpleRequestFile(const char *title, const char *filename,
329                                                                   gboolean savedialog, GCallback ok_cb,
330                                                                   GCallback cancel_cb,
331                                                                   PurpleAccount *account, const char *who, PurpleConversation *conv,
332                                                                   void *user_data)
334         id                                      requestController = nil;
335         NSString                        *titleString = (title ? [NSString stringWithUTF8String:title] : nil);
336         
337         if (titleString && [titleString isEqualToString:[NSString stringWithFormat:[NSString stringWithUTF8String:_("Export Sametime List for Account %s")], 
338                                                                                                          purple_account_get_username(account)]]) {
339                 NSSavePanel *savePanel = [NSSavePanel savePanel];
340                 
341                 if ([savePanel runModalForDirectory:nil file:nil] == NSOKButton) {
342                         ((PurpleRequestFileCb)ok_cb)(user_data, [[savePanel filename] UTF8String]);
343                 }
345         } else if (titleString && [titleString isEqualToString:[NSString stringWithFormat:[NSString stringWithUTF8String:_("Import Sametime List for Account %s")],
346                                                                                                                         purple_account_get_username(account)]]) {
347                 NSOpenPanel *openPanel = [NSOpenPanel openPanel];
348                 
349                 if ([openPanel runModalForDirectory:nil file:nil types:nil] == NSOKButton) {
350                         ((PurpleRequestFileCb)ok_cb)(user_data, [[openPanel filename] UTF8String]);
351                 }
353         } else {
354                 PurpleXfer *xfer = (PurpleXfer *)user_data;
355                 if (xfer) {
356                         PurpleXferType xferType = purple_xfer_get_type(xfer);
357                         
358                         if (xferType == PURPLE_XFER_RECEIVE) {
359                                 AILog(@"*** WARNING: File request: %s from %s on IP %s which wasn't handled by the file-recv-request signal",
360                                           xfer->filename,xfer->who,purple_xfer_get_remote_ip(xfer));
361                                 /* We should never get here.  The file-recv-request signal is posted before we could.  We handle that signal
362                                  * (in adiumPurpleSignals) and set a local filename when we do to prevent being prompted via the request_file() ui op.
363                                  */
364                                 
365                         } else if (xferType == PURPLE_XFER_SEND) {
366                                 /*
367                                  * Um, yes, we've already set the local filename... which should be the same as the file name for the transfer itself...
368                                  * and we do, in fact, want to send. Call the OK callback immediately.
369                                  */
370                                 if (xfer->local_filename != NULL && xfer->filename != NULL) {
371                                         AILog(@"PURPLE_XFER_SEND: %x (%s)",xfer,xfer->local_filename);
372                                         ((PurpleRequestFileCb)ok_cb)(user_data, xfer->local_filename);
373                                 } else {
374                                         ((PurpleRequestFileCb)cancel_cb)(user_data, xfer->local_filename);
375                                         [[SLPurpleCocoaAdapter sharedInstance] displayFileSendError];
376                                 }
377                         }
378                 }
379         }
380         
381         AILog(@"adiumPurpleRequestFile() returning %@",(requestController ? requestController : [NSNull null]));
382         return (requestController ? requestController : [NSNull null]);
386  * @brief Purple requests that we close a request window
388  * This is not sent after user interaction with the window.  Instead, it is sent when the window is no longer valid;
389  * for example, a chat invite window after the relevant account disconnects.  We should immediately close the window.
391  * @param type The request type
392  * @param uiHandle must be an id; it should either be NSNull or an object which can respond to close, such as NSWindowController.
393  */
394 static void adiumPurpleRequestClose(PurpleRequestType type, void *uiHandle)
396         id      ourHandle = (id)uiHandle;
397         AILog(@"adiumPurpleRequestClose %@ (%i)",uiHandle,[ourHandle respondsToSelector:@selector(purpleRequestClose)]);
398         if ([ourHandle respondsToSelector:@selector(purpleRequestClose)]) {
399                 [ourHandle purpleRequestClose];
401         } else if ([ourHandle respondsToSelector:@selector(closeWindow:)]) {
402                 [ourHandle closeWindow:nil];
403         }
406 static void *adiumPurpleRequestFolder(const char *title, const char *dirname, GCallback ok_cb, GCallback cancel_cb,
407                                                                           PurpleAccount *account, const char *who, PurpleConversation *conv,
408                                                                           void *user_data)
410         AILog(@"adiumPurpleRequestFolder");
412         return NULL;
415 static PurpleRequestUiOps adiumPurpleRequestOps = {
416     adiumPurpleRequestInput,
417     adiumPurpleRequestChoice,
418     adiumPurpleRequestAction,
419     adiumPurpleRequestFields,
420         adiumPurpleRequestFile,
421     adiumPurpleRequestClose,
422         adiumPurpleRequestFolder
425 PurpleRequestUiOps *adium_purple_request_get_ui_ops()
427         return &adiumPurpleRequestOps;
430 @implementation ESPurpleRequestAdapter
432 + (void)requestCloseWithHandle:(id)handle
434         AILog(@"purpleThreadRequestCloseWithHandle: %@",handle);
435         purple_request_close_with_handle(handle);
438 @end